Skip to content

Number input#23842

Merged
alice-i-cecile merged 15 commits intobevyengine:mainfrom
viridia:number_input
Apr 21, 2026
Merged

Number input#23842
alice-i-cecile merged 15 commits intobevyengine:mainfrom
viridia:number_input

Conversation

@viridia
Copy link
Copy Markdown
Contributor

@viridia viridia commented Apr 17, 2026

Objective

Part of #19236

Solution

A numeric text input widget. This includes:

  • Colored stripes ("sigils") for designating X, Y and Z input fields.
  • Character filtering
  • ValueChange events

Not supported:

  • Validation error events
  • Range and precision options

Additional changes in this PR:

  • Added small labels (related to text inputs only insofar as numeric inputs have small labels above them in the editor design).
  • Refactored labels to be simple spans instead of nested entities - this required a non-inherited text font theme component, so we renamed the existing component to have the word "inherited" in the name.
  • Consolidated the way focus outlines work for text inputs and other widgets
  • I had to make changes to Slider and other widgets to support the is_final flag in ValueChange. This is necessary to allow listeners the choice between spammy and not-spammy updates when listening to widget outputs.

Testing

Manual testing

Showcase

numeric_input

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-UI Graphical user interfaces, styles, layouts, and widgets M-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 17, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in UI Apr 17, 2026
@alice-i-cecile alice-i-cecile added X-Uncontroversial This work is generally agreed upon D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes labels Apr 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@Atlas16A
Copy link
Copy Markdown

range and percision should prob be added but im fine with that coming in a later PR

@alice-i-cecile alice-i-cecile added this to the 0.19 milestone Apr 19, 2026
Copy link
Copy Markdown
Contributor

@jordanhalase jordanhalase left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts on this

Comment thread crates/bevy_feathers/src/controls/number_input.rs Outdated
Copy link
Copy Markdown
Contributor

@kfc35 kfc35 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed everything. The example seems to work fine for me. Just have a few comments/questions

{
drag_end.propagate(false);
if state.0 && !disabled {
let local_pos = transform.try_inverse().unwrap().transform_point2(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I was looking if some of this logic has been extracted into a function (calculating a normalized local position), and there is a function on ComputedNode that does parts of this logic: normalize_point

Just for your consideration. I’ve noticed similar logic used in color_plane

/// Component which causes the color of a text span to be set based on a theme color. Unlike
/// [`InheritableThemeTextColor`], this can work when set directly on the text span entity, and is
/// not inherited.
// TODO: This is necessary because an entity with Propagate doesn't update itself, only its
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the action item in this TODO?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The real action item here is not to use Propagate, but something else. However, that's a much larger discussion.

Comment thread crates/bevy_feathers/src/controls/number_input.rs Outdated
Comment thread crates/bevy_feathers/src/controls/number_input.rs Outdated
}

/// Observer function which updates the [`NumberInputValue`] in response to a [`ValueChange`] event.
pub fn number_input_self_update(value_change: On<ValueChange<f32>>, mut commands: Commands) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure if this is a plausible scenario that merits fixing, but If focus is not on the EditableText, this particular observer is hooked in, and NumberInputValue is inserted by some other system, does this make a loop?

  1. NumberInputValue is inserted
  2. number_input_on_value_change is triggered On<Insert, NumberInputValue>
  3. Because EditableText does not have focus, it queues editable_text edits
  4. apply_text_edits emits a TextEditChange
  5. number_input_on_text_change is triggered On<TextEditChange>
  6. number_input_on_text_change emits a ValueChange
  7. number_input_self_update is triggered On<ValueChange<f32>
  8. A NumberInputValue is inserted

I assume because it requires that focus not be on the EditableText that this scenario probably won’t happen, but figured I’d bring it up if it’s actually important or to clear up if I’m misunderstanding something

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could avoid this by changing the self update function to only update the value if the value has changed. This would break the cycle.

Comment thread crates/bevy_ui_widgets/src/slider.rs Outdated
drag_end.propagate(false);
commands.entity(slider_ent).remove::<Pressed>();
if drag.dragging {
if drag.dragging && !disabled {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if InteractionDisabled is added middrag, won't it get stuck with Pressed and drag.dragging = true.

Same with the others, shouldn't we remove Pressed from a control on release, even if it has InteractionDisabled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good idea.

/// Used to indicate what format of numbers we are editing. This primarily affects the type
/// of [`ValueChange`] event that is emitted.
#[derive(Component, Default, Clone, Copy)]
pub enum NumberFormat {
Copy link
Copy Markdown
Contributor

@ickshonpe ickshonpe Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of a value input design with an InputValuePlugin<T> for each supported value type T, rather than an enum discriminator. So users could have an input for any type that can be parsed from a string. Then it could support a Val input for instance where values like "10px" or "25%" are both valid, for example.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not against merging this, and leaving discussions about generalization until later though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer that design too :)

@alice-i-cecile alice-i-cecile added D-Complex Quite challenging from either a design or technical perspective. Ask for help! and removed D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes labels Apr 20, 2026
Copy link
Copy Markdown
Contributor

@kfc35 kfc35 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is in a good enough state to merge and to have some follow ups later for anything still pending (except for my comment about drag end and InteractionDisabled).

Comment thread crates/bevy_ui_widgets/src/slider.rs
Comment thread crates/bevy_feathers/src/controls/number_input.rs Outdated
Comment thread crates/bevy_feathers/src/controls/number_input.rs Outdated
:group_body
Children [
:label("A standard group"),
:label_small("Scalar property"),
Copy link
Copy Markdown
Contributor

@kfc35 kfc35 Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running the example, there’s a bit of unexpected behavior here now:

  1. Focus on the first scalar property
  2. Update the value to something different
  3. Tab navigate to the second scalar property (copy)
    Note that, here, the second scalar property is not updated with the value from the first scalar property field
  4. Tab navigate again away from the second scalar property (or just click anything but the first scalar property to unfocus the second scalar property)
    Observe that the first scalar property is set back to the value that is in the second scalar copy property

I expected the first scalar property’s change to be reflected in the second scalar property after I tab switched focus from the first to the second scalar copy.

I suspect the cause is due to number_input_on_update not applying UpdateNumberInput to focused entities.

However, I don’t feel this is too pressing to fix in this PR if it’s not trivial to fix / would affect other actually desirable UX. This can be a follow up if this is something that should be fixed. This is a bit of a contrived example and I don’t know how much this will actually be surfaced in practice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you are seeing is a race condition between the two focus events: because the second input acquires focus before the focus-lost event is received for the first input, this blocks the second one from being updated.

Copy link
Copy Markdown
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy enough with this design now; I'd prefer to merge and iterate :)

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Apr 21, 2026
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Apr 21, 2026
Merged via the queue into bevyengine:main with commit 64fe756 Apr 21, 2026
42 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in UI Apr 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

7 participants